SalaryElements (Lohnelemente) – Entwicklerdokumentation
Diese Dokumentation beschreibt das Feature SalaryElements (deutsch „Lohnelemente") der biz.Suite auf dem Branch salary_module_extension. Ein SalaryElement ist ein konkreter, datierter Lohnposten eines Mitarbeiters für eine bestimmte Lohnart (SalaryType).
Wichtig – zwei getrennte Generierungs-Subsysteme: Auf diesem Branch existieren zwei voneinander unabhängige Mechanismen, die
SalaryElements erzeugen. Sie dürfen nicht verwechselt werden:
- Journalbasierte Generierung (das eigentliche neue Feature dieses Branches) – der
SalaryElementGenerationProcessorerzeugt Lohnelemente tagesgenau aus abgerechneten und freigegebenen Arbeitszeitsjournalen, Buchungen, Abwesenheiten und den Regeln eines Lohnvertrags (SalaryAgreement).- Zulagen-Monatsgenerierung (Altbestand, weiterhin vorhanden) – der
GenerateMounthlySalaryElementsCommanderzeugt monatliche Zulagen-Posten ausWorkerSalaryType-Zuordnungen vom Typallowance.In beiden Fällen entsteht dieselbe Entity
SalaryElement, das FeldsalaryUnitwird aber semantisch unterschiedlich befüllt (Subsystem 1: Dauer in Sekunden; Subsystem 2: der ganzzahligedefaultValueder Zuordnung).
Sie richtet sich an Backend- und Frontend-Entwickler, die das Feature pflegen, erweitern oder integrieren.
Stand: 2026-07-10
Branch: salary_module_extension (Backend & Frontend)
Repositorys:
- Backend:
biz.Suite_Backend(Symfony / Doctrine / PHP) - Frontend:
biz.Suite_Frontend(Angular / TypeScript)
Verwandte Dokumentation: Nachtschicht-Entwicklerdokumentation (das 12_12-Modell, auf dem die Nachtschicht-Behandlung des Generators aufbaut), Lohnmodul-Benutzerhandbuch.
Inhaltsverzeichnis
- Übersicht & Motivation
- Datenmodell & Entities
- Datenbank-Migrationen
- Journalbasierte Generierung (
SalaryElementGenerationProcessor) - Auslösung / Triggering der journalbasierten Generierung
- Zulagen-Monatsgenerierung (Altpfad)
- Backend-Komponenten (Controller & Repositories)
- Frontend-Komponenten im Detail
- Berechtigungssteuerung
- Durchgerechnetes Beispiel
- Fallstricke, Grenzen & offene Punkte
- Referenz: alle Fundstellen
1. Übersicht & Motivation
Das Lohnmodul soll aus den erfassten Arbeitszeiten eines Mitarbeiters die abrechnungsrelevanten Lohnposten ableiten – z. B. „8 h Normalzeit", „2 h Nachtzuschlag", „1 Feiertag" oder „Urlaub". Diese Posten sind die SalaryElements.
Zentrale Idee der journalbasierten Generierung:
- Grundlage ist das abgerechnete Arbeitszeitsjournal (
TimeProfileWorkerJournal) eines Tages. Nur wenn dieses berechnet (isCalculated) und freigegeben (isApproved) ist, gilt der Tag als „fertig" und wird generiert. - Die realen Stempelungen (
TimeControlBooking) werden in Anwesenheits-Intervalle zerlegt. - Ein Lohnvertrag (
SalaryAgreement) definiert pro Wochentag Zeitfenster (SalaryAgreementLine), die jeweils eine Lohnart (SalaryType) tragen (z. B. „22:00–06:00 → Nachtzuschlag"). - Die Überlappung von Anwesenheit und Vertrags-Zeitfenster ergibt je Lohnart eine Dauer in Sekunden, die – abzüglich Pausen – als
SalaryElementgespeichert wird. - Abwesenheiten (
Absence) an dem Tag erzeugen eigene Elemente über die Lohnart des jeweiligen Abwesenheitscodes (AbsenceCode.salaryType). - Feiertage und Nachtschicht (
12_12) werden gesondert behandelt.
Der Altpfad (Zulagen) bleibt davon unberührt: Er erzeugt weiterhin monatliche Pauschal-Zulagen aus WorkerSalaryType-Zuordnungen (Kap. 6).
2. Datenmodell & Entities
2.1 Überblick der Beziehungen
┌───────────────────────────┐
│ SalaryAgreement │ Lohnvertrag (name, description)
│ wageSalary/overTimeSalary│ + 3 SalaryType-Referenzen
│ /defaultSalary │
└───┬───────────────┬───────┘
OneToMany │ │ OneToMany workers (Worker.salaryAgreement)
salaryAgreementDays│ ▼
▼ ┌────────────┐
┌────────────────────┐│ Worker │──OneToMany──► SalaryElement
│ SalaryAgreementDay ││ salaryAgree│ (worker)
│ name = Wochentag ││ ment │
└─────────┬──────────┘└─────┬──────┘
OneToMany │ │ (Journal, Bookings, Absences …)
salaryAgreementLines │
▼ ▼
┌───────────────────────────────────────┐ ┌──────────────────────┐
│ SalaryAgreementLine │ │ TimeProfileWorker │
│ startTime/endTime, timeProfileType, │ │ Journal │
│ salaryType, minimalTime/maximalTime, │ │ isCalculated, │
│ secondarySalaryType, secondaryFactor │ │ isApproved, │
└──────────────────┬─────────────────────┘ │ profileTime, date │
ManyToOne │ salaryType └──────────────────────┘
▼
┌────────────────────────┐ ┌────────────────────┐
│ SalaryType │◄────────│ AbsenceCode │
│ type, amount, │ salary │ salaryType (NEW) │
│ minimalTime, │ Type └────────────────── ──┘
│ isStandardForHoliday, │
│ absenceCodes (OneToMany)│
└─────────┬──────────────┘
OneToMany│ salaryElements
▼
┌────────────────────────────────────────────┐
│ SalaryElement │
│ profileDate (Kalendertag des Postens) │
│ journalDate (Tag des erzeugenden Journals) │ ← NEU
│ salaryUnit (Dauer in Sekunden bzw. Wert) │
│ transferToPayroll, salaryType, worker │
└─────────────────────────────────────────────┘
(Altpfad Zulagen: WorkerSalaryType = Worker ↔ SalaryType + defaultValue)
2.2 Entity SalaryElement
Datei: biz.Suite_Backend/src/Entity/SalaryElement.php
| Feld | Typ | Bedeutung |
|---|---|---|
id | int | PK |
profileDate | datetime, nullable | Kalendertag, dem der Posten zugerechnet wird. Bei Nachtschicht kann ein Lauf Posten auf zwei Kalendertage schreiben. |
journalDate | datetime, nullable | NEU. Datum des Journals, das den Posten erzeugt hat. Dient der idempotenten Neu-Generierung (siehe Kap. 4.2). |
salaryUnit | float, nullable | Journalpfad: Dauer in Sekunden. Zulagenpfad: der defaultValue. |
transferToPayroll | bool, default false | Freigabe zur Lohnabrechnung (manuell in UI). |
salaryType | ManyToOne SalaryType | Lohnart des Postens. |
worker | ManyToOne Worker (workerid) | Mitarbeiter. |
journalDatevs.profileDate:profileDateist der Tag, für den der Posten fachlich zählt;journalDatemarkiert, welcher Generierungslauf ihn besitzt. Beide sind identisch, außer bei Nachtschichten, wo ein Journal-Tag (journalDate = T) Posten aufT-1undT(profileDate) verteilen kann. Das Feld existiert nur im Backend – das Frontend kennt und zeigt nurprofileDate.
2.3 Entity SalaryType
Datei: biz.Suite_Backend/src/Entity/SalaryType.php
| Feld | Typ | Bedeutung |
|---|---|---|
description | string, nullable | Bezeichnung |
type | string, nullable | hours / salary / allowance (frei; nur allowance relevant für den Altpfad) |
shouldIncludeInExport | bool | in Export einbeziehen |
externalSalaryType | string, nullable | externe Lohnart-Kennung |
amount | int, nullable | NEU. Zuschlag in Prozent (FE-Label „Zuschlag in %"). Wird zur Sortierung der Lohnarten genutzt (ORDER BY amount DESC), bestimmt also die Verarbeitungs-Priorität. |
minimalTime | float, nullable | NEU. Mindest-Dauer (Sekunden). Erreicht die je Lohnart summierte Überlappung diesen Wert nicht, entsteht kein Posten. |
isStandardForHoliday | bool, default false | NEU. Kennzeichnet die Lohnart, die an Feiertagen als virtueller Ganztagsposten verwendet wird. |
absenceCodes | OneToMany AbsenceCode | NEU. Rückbeziehung zu den Abwesenheitscodes, die diese Lohnart nutzen. |
salaryElements | OneToMany SalaryElement | zugehörige Posten |
2.4 Lohnvertrag: SalaryAgreement, SalaryAgreementDay, SalaryAgreementLine
-
SalaryAgreement(SalaryAgreement.php) –name,description; OneToManysalaryAgreementDays,salaryAgreementLines,timeProfiles,workers; drei SalaryType-ReferenzenwageSalary,overTimeSalary,defaultSalary. Ein Mitarbeiter wird überWorker.salaryAgreement(ManyToOne) einem Vertrag zugeordnet. -
SalaryAgreementDay(SalaryAgreementDay.php) –name= Wochentagsname (englisch, z. B.Monday), OneToManysalaryAgreementLines. Repräsentiert die Regeln eines Wochentags. -
SalaryAgreementLine(SalaryAgreementLine.php) – ein einzelnes Zeitfenster einer Regel:Feld Bedeutung startTime/endTimeZeitfenster (nur Uhrzeit) salaryTypeLohnart, die für Überlappungen in diesem Fenster gutgeschrieben wird timeProfileTypeTyp-Zuordnung minimalTime/maximalTimeGrenzen (float; derzeit vom Processor nicht ausgewertet, siehe Kap. 11) secondarySalaryType/secondaryFactorSekundär-Lohnart / Faktor (derzeit vom Processor nicht ausgewertet) salaryAgreementDay/salaryAgreementZugehörigkeit
Kernrolle im Generator: Für jeden Kalendertag sucht der Processor den
SalaryAgreementDaydes Vertrags anhand des Wochentags und lädt dessenSalaryAgreementLines. Die Überlappung von Anwesenheit und Line-Zeitfenster wird dersalaryTypeder Line gutgeschrieben.
2.5 AbsenceCode.salaryType (neue Verknüpfung)
Datei: AbsenceCode.php – neu: #[ORM\ManyToOne(inversedBy: 'absenceCodes')] private ?SalaryType $salaryType. Eine Abwesenheit (Absence) verweist über ihren AbsenceCode auf eine Lohnart, mit der der Abwesenheitspfad des Generators arbeitet (Kap. 4.3).
2.6 PreCalculatedBooking (View-Model)
Datei: src/ViewModel/PreCalculatedBooking.php
Kein Doctrine-Entity, sondern ein reines Rechenobjekt: from, to (Carbon), isBreakTime (bool), timeLeft (float, Sekunden), salaryOverlaps (array<int typeId, float sekunden>). Repräsentiert ein Anwesenheits-Intervall zwischen zwei Stempelungen sowie dessen Überlappungen pro Lohnart.
2.7 WorkerSalaryType (nur Altpfad)
Datei: WorkerSalaryType.php – Join-Entity Worker ↔ SalaryType mit defaultValue (int). Nur für die Zulagen-Monatsgenerierung relevant (Kap. 6); der SalaryElementGenerationProcessor verwendet sie nicht.
3. Datenbank-Migrationen
Neue/relevante Migrationen dieses Branches:
| Migration | Inhalt |
|---|---|
| Version20260302140928.php | salary_type ADD amount INT, ADD minimal_time DOUBLE; workers ADD salary_agreement_id (+ FK) |
| Version20260319151007.php | salary_type ADD is_standard_for_holiday TINYINT(1) DEFAULT 0 |
| Version20260323104345.php | absencecodes ADD salary_type_id INT (+ FK) |
| Version20260630141454.php | salary_element ADD journal_date DATETIME |
Bestehende Tabellen (Altbestand): salary_element, salary_type, worker_salary_type, salary_agreement, salary_agreement_day, salary_agreement_line.
4. Journalbasierte Generierung (SalaryElementGenerationProcessor)
Datei: biz.Suite_Backend/src/Processor/Salary/SalaryElementGenerationProcessor.php
Dies ist das Herzstück des Features. Der Processor ist ein Symfony-Service (autowired mit EntityManagerInterface).
4.1 Öffentliche Einstiegspunkte
| Methode | Zweck |
|---|---|
generateSalaryElementsForDayForWorker(Carbon $day, Worker $worker) | Ein Mitarbeiter, ein Tag. Flusht danach. |
generateSalaryElementsForDay(Carbon $day) | Ein Tag, alle Mitarbeiter vom Typ employee (WorkerRepository::getWorkersByType('employee')). Flush pro Mitarbeiter. |
generateSalaryElementsForMonthForWorker(int $month, Worker $worker, ?int $year) | Ein Mitarbeiter, ganzer Monat. Delegiert an …ForWorkers. |
generateSalaryElementsForMonth(int $month, ?int $year) | Ganzer Monat, alle employee. |
generateSalaryElementsForMonthForWorkers(array $workers, int $month, ?int $year) | Sammel-Einstieg: iteriert je Mitarbeiter über alle Tage des Monats (getMonthRange), flusht pro Mitarbeiter, fängt Fehler pro Mitarbeiter ab und liefert eine Liste von failures (workerId, personnelNumber, error) zurück. Arbeitet mit Worker-IDs und re-fetcht die Instanz, um auch nach einem EM-Reset weiterarbeiten zu können. |
Alle Wege münden in die private Kernmethode generate(Carbon $targetDate, Worker $worker).
4.2 Die Kernmethode generate() – Voraussetzung Journal
$journal = $journalRepo->findOneBy([
'date' => $targetDate,
'worker' => $worker,
'isCalculated' => true,
'isApproved' => true
]);
// Kein berechnetes UND freigegebenes Journal -> Tag wird übersprungen
if (!$journal) {
return;
}
✅ Direkte Antwort auf die häufige Frage: Lohnelemente werden über diesen Pfad ausschließlich für Tage erzeugt, für die ein Journal existiert, das sowohl
isCalculated = trueals auchisApproved = trueist. Fehlt das Journal oder ist eines der Flags nicht gesetzt, gilt der Tag als „noch nicht bereit" und es wird nichts generiert.
4.2 Idempotenz – Löschen vor Neuanlage
Direkt nach dem Journal-Check:
$this->deleteSalaryElementsForJournalDay($worker, $targetDate);
deleteSalaryElementsForJournalDay entfernt alle Elemente, deren journalDate auf targetDate fällt (SalaryElementRepository::getByWorkerAndJournalDate). Damit ist ein erneuter Lauf für denselben Tag idempotent – auch wenn eine Nachtschicht Posten auf zwei Kalendertage (profileDate) verteilt hat, werden sie über den gemeinsamen journalDate sauber zurückgesetzt und neu erzeugt.
4.3 Abwesenheitspfad (generateFromAbsence)
$absences = $absenceRepo->getAbsencesForWorkerInRangeByType($worker, $start, $end)->getResult();
if (count($absences) > 0) {
$this->generateFromAbsence($targetDate, $worker, $absences, $journal);
}
Für jede Abwesenheit am Tag:
- Lohnart =
absence->getAbsenceCode()?->getSalaryType()(die neue Verknüpfung aus Kap. 2.5). - Dauer =
min(journal.profileTime, absenceDuration)– die Abwesenheit wird auf die Sollzeit des Tages gedeckelt. - Es entsteht ein
SalaryElementmitprofileDate = journalDate = targetDate,salaryUnit = duration(Sekunden).
Wichtig: Der Abwesenheitspfad läuft vor der Buchungsprüfung und unabhängig von einem Lohnvertrag. Abwesenheits-Posten entstehen also auch, wenn der Mitarbeiter keinen
SalaryAgreementhat oder keine Buchungen vorliegen.
4.4 Buchungspfad – Ablauf
Nach dem Abwesenheitspfad:
$isNightShift = $journal->getTimeProfile()?->getCalculationPeriod() === '12_12';
[$windowStart, $windowEnd] = $this->getCalculationPeriodRange($targetDate, $isNightShift);
$bookings = $bookingRepo->findInRangeFiltered($windowStart, $windowEnd, $worker);
if (count($bookings) < 1 || !$worker->getSalaryAgreement()) {
return; // ohne Buchungen ODER ohne Lohnvertrag: kein Buchungspfad
}
- Zeitfenster (
getCalculationPeriodRange): StandardT 00:00 … T 23:59; Nachtschicht (12_12)T-1 12:00 … T 12:00. - Anwesenheits-Intervalle bilden (
createPreCalculatedBookings): Aufeinanderfolgende Stempelungen werden paarweise zuPreCalculatedBooking-Intervallen verbunden.- Beginnt eine Lücke mit einem Check-Out, ist es Abwesenheit (der Mitarbeiter ist gegangen) → wird übersprungen.
- Ist die vorherige Stempelung weder Check-In noch Check-Out (also ein Pausen-Typ), wird das Intervall als Pause markiert (
isBreakTime = true). timeLeft= Länge des Intervalls in Sekunden.
- An Kalendertagen splitten (
splitPreCalculatedBookingsAtCalendarDay): Intervalle, die über Mitternacht laufen, werden an den Tagesgrenzen zerschnitten – denn jeder Kalendertag kann eigeneSalaryAgreementLine-Regeln haben. - Nach Kalendertag gruppieren (
slicesByDay, Schlüssel =from->toDateString()). - Pro Kalendertag (
foreach $slicesByDay): a.SalaryAgreementDayper Wochentag (calendarDay->dayName) undSalaryAgreementLines laden. b. Feiertag (publicHolidayRepo->findOneBy(['date' => $calendarDay])): existiert einer und gibt es eine Lohnart mitisStandardForHoliday, wird eine virtuelle Ganztags-Line (00:00–23:59) mit dieser Lohnart hinzugefügt. c.processOverlaps($daySlices, $salaryAgreementLines)– Überlappungen Anwesenheit × Line. d.sumOverlapTimes(...)– Summierung je Lohnart inkl.minimalTime-Schwelle. e.calculateBreakTime(...)– abzuziehende Pausen je Lohnart. f.createSalaryElements(...)– Posten schreiben (profileDate = calendarDay,journalDate = targetDate).
4.5 Überlappung & Summierung
processOverlaps (Buchungen × Vertrags-Lines): Für jedes Anwesenheits-Intervall und jede Line werden Start/Ende der Line auf das Datum des Intervalls gestempelt (Ende vor Start → +1 Tag). Bei Überlappung wird die Überlappungsdauer (calculateOverlap = spätester Start bis frühestes Ende) je salaryType.id in slice->salaryOverlaps akkumuliert.
sumOverlapTimes (über nach amount DESC sortierte Lohnarten): Je Lohnart wird total = Σ min(slice.timeLeft, slice.salaryOverlaps[typeId]) gebildet. Erreicht total die salaryType.getMinimalTime()-Schwelle nicht, wird die Lohnart verworfen. Andernfalls fließt total in salaryTypeArray[typeId] ein.
4.6 Pausenberechnung (calculateBreakTime)
Ermittelt je Lohnart die abzuziehende Pausenzeit als max(geplante Pause, tatsächlich genommene Pause):
- Geplante Pausen stammen aus den
TimeProfileDayLines des Journals (getJournalDayLines), gefiltert auf Pausen-Typen (timeProfileType->isCalcJustifiedAbsence()), überlappt mit den Vertrags-Lines überprocessOverlapsForDayLines. Eine geplante Pause zählt nur für die Zeit, in der der Mitarbeiter anwesend war (presentDuration). - Genommene Pausen stammen aus den
PreCalculatedBookings mitisBreakTime = true. - Nachtschicht: Zeiten werden über
mapToNightShiftTimeline(Uhrzeit≥ 12:00→ Vortag) auf die Noon-to-Noon-Zeitachse projiziert; eine geplante Pause wird nur dem Kalendertag zugerechnet, auf den sie tatsächlich fällt.
4.7 Posten schreiben (createSalaryElements)
foreach ($salaryTypeArray as $typeId => $timeLeft) {
if ($timeLeft < 1 || !$worker->getSysClient()) continue;
$salaryType = /* per id */;
$time = $timeLeft - ($breakTimes[$typeId] ?? 0); // Pausen abziehen
$salaryElement = (new SalaryElement())
->setProfileDate($profileDate) // Kalendertag der Slice
->setJournalDate($journalDate) // targetDate des Laufs
->setSalaryType($salaryType)
->setSalaryUnit($time) // Sekunden (Netto-Arbeitszeit)
->setWorker($worker);
$salaryElement->setSysClient($worker->getSysClient());
$this->entityManager->persist($salaryElement);
}
salaryUnit ist hier die Netto-Dauer in Sekunden (Überlappung minus Pause). Ein Flush erfolgt durch den jeweiligen öffentlichen Einstiegspunkt.
4.8 Nachtschicht & Feiertag – Zusammenfassung
- Nachtschicht (
12_12): breiteres Buchungsfenster (T-1 12:00 … T 12:00) und Noon-Pivot-Projektion (mapToNightShiftTimeline), damit eine über Mitternacht laufende Schicht korrekt einem oder zwei Kalendertagen zugeordnet wird. Baut auf demselben Prinzip auf wie die Nachtschicht-Dokumentation. - Feiertag: virtueller Ganztagsposten über die
isStandardForHoliday-Lohnart.
5. Auslösung / Triggering der journalbasierten Generierung
5.1 REST-Endpoint (primärer Trigger)
Datei: SalaryElementController.php (neu auf diesem Branch)
#[Route(path: 'api/salaryElements/generate/worker/{id}', name: 'generate_salary_elements_for_worker', methods: ['POST'])]
public function generateSalaryElementsForWorker(Worker $worker, Request $request, SalaryElementGenerationProcessor $processor): JsonResponse
{
$month = (int) $request->get('month');
$processor->generateSalaryElementsForMonthForWorker($month, $worker);
return new JsonResponse(null, Response::HTTP_CREATED);
}
POST api/salaryElements/generate/worker/{id}?month={1-12}, leerer Body, Antwort201 Created.- Generiert für einen Mitarbeiter den ganzen Monat (Jahr = aktuelles Jahr, da nicht übergeben).
- Kein Cron/Scheduler: Wie schon der Altpfad ist auch dieser Endpoint nicht zeitgesteuert – die Generierung wird benutzerinitiiert über die Oberfläche ausgelöst (siehe Kap. 8.2).
5.2 TestCommand
Datei: src/Command/TestCommand.php injiziert den Processor und ruft ihn auf.
⚠️ Achtung – Bruchstelle:
TestCommandruftgenerateSalaryElementsForWorkerForDay($worker, new Carbon())auf, diese Methode existiert im Processor jedoch nicht (dort heißt siegenerateSalaryElementsForDayForWorker(Carbon $day, Worker $worker)– anderer Name und vertauschte Argumentreihenfolge). Der Command ist damit in der aktuellen Form nicht lauffähig und dient offenbar nur zu Testzwecken. Bei einer echten Nutzung/Portierung zwingend anpassen.
6. Zulagen-Monatsgenerierung (Altpfad)
Datei: GenerateMounthlySalaryElementsCommand.php
Dieses ältere, unabhängige Subsystem existiert unverändert weiter. Command-Name: workers:generate:saleryelements:allowance (Tippfehler „Mounthly"/„salery" im Original beibehalten).
Kurzablauf:
- Zielmonate: aktueller + 2 folgende (jeweils Monatserster als
profileDate). - Über alle aktiven Mitarbeiter; je Mitarbeiter alle
WorkerSalaryType-Zuordnungen. - Nur wenn
salaryType.type === 'allowance': je Zielmonat einSalaryElement, sofern noch keines existiert (findOneBy(worker, salaryType, profileDate)→ idempotent). salaryUnit = workerSalaryType.getDefaultValue()(int!),transferToPayroll = false,sysClientvom Worker.
Abgrenzung: Dieser Pfad kennt weder Journale noch
isApproved/isCalculated, nochjournalDate, noch Lohnverträge. Er setztjournalDatenicht (bleibtnull) und befülltsalaryUnitmit einer Pauschale, nicht mit Sekunden. Details und Fallstricke dieses Altpfads siehe Kap. 11.
7. Backend-Komponenten (Controller & Repositories)
7.1 SalaryElementController
Datei: SalaryElementController.php
| Route | HTTP | Zweck |
|---|---|---|
api/salaryElements | GET | Paginierte Liste; start/end oder date (auf Monat expandiert). |
api/salaryElements/{id} | GET | Einzelnes Element. |
api/salaryElements/worker/{id} | GET | Liste eines Mitarbeiters im Bereich. |
api/salaryElements | POST | Anlegen (createOrUpdate). |
api/salaryElements/{id} | PUT | Aktualisieren (gleiche Logik wie POST). |
api/salaryElements/{id} | DELETE | Löschen (204). |
api/salaryElements/generate/worker/{id} | POST | NEU – journalbasierte Generierung, ?month= (Kap. 5.1). |
7.2 Repositories
SalaryElementRepository(Datei):getByWorkerAndRange(worker|null, start, end)– Filter überprofileDate(Listen/UI).getByWorkerAndJournalDate(worker, journalDate)– NEU – alle Elemente eines Journal-Laufs (Filter überjournalDate), Basis der Idempotenz.
AbsenceRepository::getAbsencesForWorkerInRangeByType–typeist jetzt optional (?string = null); ohnetypeentfällt derisHoliday-Filter (der Generator ruft es ohne Typ auf).WorkerRepository::getWorkersByType('employee')– aktive Mitarbeiter nach Rolle (isEmployee/isContractor/…); Grundmenge der „alle Mitarbeiter"-Einstiege.SalaryTypeRepository– u. a.findByWorker, sowiefindBy([], ['amount' => 'DESC'])(im Processor genutzt).- Weitere:
SalaryAgreementDayRepository,SalaryAgreementLineRepository,TimeControlBookingRepository::findInRangeFiltered,PublicHolidayRepository,TimeProfileDayRepository/TimeProfileDayLineRepository.
7.3 SalaryAgreementLineController, SalaryTypeController, WorkerSalaryTypesController
SalaryAgreementLineController– CRUD der Vertrags-Lines (auf diesem Branch angepasst).SalaryTypeController– CRUD + Elasticsearch-Sucheapi/find/salaryTypes(Filtertype,excludeByWorker).WorkerSalaryTypesController– CRUD der Zulagen-Zuordnungen (Altpfad).
8. Frontend-Komponenten im Detail
Pfade relativ zu biz.Suite_Frontend/src/app/main/apps/. Zwei App-Bereiche: organization-management/salary-managment/ (Admin) und hrm/ (mitarbeiterbezogen).
8.1 Frontend-Modelle
| Modell | Datei | Neue/relevante Felder |
|---|---|---|
SalaryType | salary-managment/salary-types/salary-type.model.ts | + amount: number, minimal_time: number (Sekunden), is_standard_for_holiday?: boolean. Kein absence_codes. |
SalaryElement | salary-managment/salary-elements/salary-element.ts | unverändert; kein journal_date. salary_unit = Sekunden. |
SalaryAgreement | salary-agreements/salary-agreement.model.ts | name, description, salary_agreement_days?, default_salary?, wage_salary?, over_time_salary? |
SalaryAgreementDay | salary-agreement-day/salary-agreement-day.ts | id, name |
SalaryAgreementLine | salary-agreement-day/salary-agreement-line.ts | start_time, end_time, salary_type, time_profile_type, minimal_time, maximal_time, secondary_salary_type, secondary_factor, salary_agreement(_day) |
Worker | hrm/workers/worker.model.ts | + salary_agreement?: SalaryAgreement |
AbsenceCode | hrm/absenceCode/absenceCode.model.ts | + salary_type: SalaryType |
Zeitwert-Handling:
salary_unitundminimal_timewerden als Sekunden persistiert, aber alsHH:mm(type="time") editiert. HilfsfunktionendurationToSeconds/secondsToDurationkonvertieren (insalary-element-dialogundsalary-type-dialog). Die Liste rendertsalary_unitviasecondsToDuration()(z. B.08:00 Std.).
8.2 Generierungs-Dialog (SalaryElementGenerationDialogComponent)
Verzeichnis: salary-managment/salary-elements/salary-element-generation-dialog/ (neu)
- Ausgelöst über einen „Lohnelemente generieren"-Button (
autorenew-Icon) insalary-elements.component.html, gated durchuserIsAllowed(). - Monatsauswahl auf das aktuelle Jahr beschränkt (
startView="year"); Mehrfach-Mitarbeiterauswahl (Suche viasearchWorker). generate()bestätigt überDeleteConfirmDialogV2Service(i18nSalaryElements.generateConfirm/generateConfirmSub), dannrunGeneration():month = getMonth()+1und ein Request pro Mitarbeiter inPromise.all.- Service
SalaryElementsSerivice.generateSalaryElements(workerId, month)→POST api/salaryElements/generate/worker/{workerId}?month={month}(leerer Body).
Warnhinweis-Semantik: Der Bestätigungstext weist darauf hin, dass bestehende Lohnelemente im gewählten Zeitraum ersetzt werden – exakt das Verhalten des Processors (Delete-vor-Neuanlage, Kap. 4.2).
8.3 Salary-Element-Listen
- Admin-Liste (
salary-elements.component): Spalten['worker','salaryType','profileDate','salaryUnit','transferToPayroll','edit']; Monats-Datumsfilter (SalaryElements.profileDate); CRUD-Dialog + Generierungs-Button.salaryUnitals Dauer gerendert. - Mitarbeiter-Liste (
worker-salary-elements.component, eingebettet in Worker-Details): wie oben ohneworker-Spalte; GETsalaryElements/worker/{id}. - Zulagen-Report (
hrm/worker-allowances): Monats-Picker + Typfilter (find/salaryTypes?type=allowance), Inline-Edit von Betrag/Freigabe. Kein Generierungs-Button (reiner Altpfad-Report).
8.4 Lohnart-Dialog (SalaryTypeDialogComponent)
- Felder:
description(Pflicht),amount(Pflicht, Label „Zuschlag in %"),minimal_time(Label „Minimum",type="time", Sekunden-Konvertierung),shouldIncludeInExport,externalSalaryType,is_standard_for_holiday(Label „Standard für Feiertage"). - Cross-Field-Regel: Wird
is_standard_for_holidayaktiviert, wirdshouldIncludeInExportauftrueerzwungen und deaktiviert; Deaktivieren gibt es wieder frei. types = ['hours','salary','allowance'](einzige Quelle der Typwerte).
8.5 Lohnverträge (salary-agreements/, salary-agreement-day/)
- Liste
salary-agreements.component: Spalten['name','description','edit']; Zeilen → Detailroute; neuer „Mitglieder"-Button (openWorkerDialog). - Vertrags-Dialog
salary-agreements/dialogs/salary-agreement-dialog/: Feldername,description,over_time_salary_type(vorbefüllt). Hinweis:getTransferObjectsendet nurname/description– die Salary-Type-Referenzen werden im Dialog gelesen, aber nicht mitgespeichert. - Mitglieder-Dialog
salary-agreements/dialogs/salary-agreement-worker-dialog/: ordnet Mitarbeiter dem Vertrag zu viaPUT api/v2/workers/{workerid}({ salary_agreement: {id}|null });GET api/v2/workers/bySalaryAgreement/{id}. - Tages-Lines
salary-agreement-day.component: Spalten['description','salaryType','startTime','endTime','edit']; Line-Editor mitsalary_type(Pflicht),start_time/end_time(type=time, Pflicht),description. - Service
salary-agreements.service.ts:salaryAgreements(CRUD),find/salaryAgreements,v2/workers(Zuordnung).
8.6 Abwesenheitscode ↔ Lohnart
hrm/absenceCode/absenceCode.model.ts: neues Feldsalary_type.absenceCodeDialog: neuer suchbarer Lohnart-Select (api/find/salaryTypes?query=); Payload enthältsalary_type: { id }. Dieser Link speist den Abwesenheitspfad des Generators (Kap. 4.3).
8.7 i18n
Nur drei neue Keys unter SalaryElements (salary-managment/i18n/de.ts, analog en/pl):
generate : 'Lohnelemente generieren'
generateConfirm : 'Bestehende Lohnelemente im gewählten Zeitraum werden … neu generiert und ersetzt. Fortfahren?'
generateConfirmSub : 'Bereits generierte Lohnelemente in diesem Zeitraum gehen verloren.'
Die Labels der neuen SalaryType-Felder (Zuschlag in %, Minimum, Standard für Feiertage) und der Abwesenheitscode-Select (Lohnart) sind hartkodiertes Deutsch in den Templates – Lokalisierungslücke.
9. Berechtigungssteuerung
Datei: salary-managment/salary-elements/salary-element-creation-access.service.ts
getAuthorization()→ POSTauth/me; setzthasAccess = true, sobald ein Permission-Name auf/create_salary_elements/gimatcht.- Gated (nur UI-Sichtbarkeit): Create-FAB, Edit/Delete und der neue Generierungs-Button. Erforderliche Permission:
create_salary_elements. Serverseitige Durchsetzung erfolgt unabhängig davon.
10. Durchgerechnetes Beispiel (journalbasiert)
Setup: Mitarbeiter mit SalaryAgreement. Vertragstag „Friday" hat zwei Lines: 06:00–22:00 → Normalzeit, 22:00–06:00 → Nachtzuschlag. Lohnart „Feiertag" hat isStandardForHoliday = true. Tag T = 2026-07-10 (Freitag), Standardprofil (0_24).
Aufruf: POST api/salaryElements/generate/worker/{id}?month=7 → iteriert alle Julitage; für den 10.07.:
- Journal
10.07.gefunden mitisCalculated = true,isApproved = true→ weiter (sonst Skip). - Vorhandene Elemente mit
journalDate = 10.07.gelöscht. - Keine Abwesenheit am Tag → kein Abwesenheitspfad.
- Buchungen im Fenster
10.07. 00:00…23:59: Check-In07:58, Pausen-Buchungen12:00/12:30, Check-Out20:03. - Anwesenheits-Intervalle gebildet, Pause
12:00–12:30alsisBreakTime. - Kein Feiertag am 10.07. → keine virtuelle Line.
- Überlappung mit Line
06:00–22:00 (Normalzeit): ~12 h; mit22:00–06:00 (Nachtzuschlag): 0. sumOverlapTimes: Normalzeit-Total ≥minimalTime→ bleibt; Pause 30 min wird incalculateBreakTimeermittelt.createSalaryElements: einSalaryElement(Normalzeit),profileDate = 10.07.,journalDate = 10.07.,salaryUnit ≈ 12 h − 0,5 h = 11,5 hin Sekunden.
Feiertagsfall: Wäre der 10.07. ein Feiertag, käme zusätzlich eine virtuelle Ganztags-Line mit der isStandardForHoliday-Lohnart hinzu → entsprechender Feiertagsposten.
Nachtschicht-Fall (12_12): Fenster 09.07. 12:00 … 10.07. 12:00; über Mitternacht laufende Intervalle würden an 00:00 gesplittet und ggf. auf profileDate 09.07. und 10.07. verteilt – beide mit journalDate = 10.07..
11. Fallstricke, Grenzen & offene Punkte
| Thema | Beschreibung | Ort |
|---|---|---|
| Zwei Subsysteme, eine Entity | Journalpfad (Sekunden, journalDate gesetzt) und Zulagenpfad (Pauschale, journalDate = null) schreiben beide in SalaryElement. salaryUnit ist kontextabhängig zu interpretieren. | Processor vs. Command |
Voraussetzung isCalculated + isApproved | Journalpfad generiert nur für Tage mit berechnetem und freigegebenem Journal; sonst stiller Skip ohne Rückmeldung. | generate() Z. 144–150 |
TestCommand defekt | Ruft nicht existente Methode generateSalaryElementsForWorkerForDay (richtiger Name: generateSalaryElementsForDayForWorker, andere Arg-Reihenfolge). Nicht lauffähig. | TestCommand.php |
| Kein Scheduler | Weder Journal- noch Zulagenpfad sind zeitgesteuert; Journalpfad ist rein UI-getriggert (pro Mitarbeiter, pro Monat). Eine „alle Mitarbeiter"-Massengenerierung ist im Backend vorhanden (generateSalaryElementsForMonth), aber nicht per Route exponiert. | Controller/Processor |
| Line-Felder ungenutzt | SalaryAgreementLine.minimalTime/maximalTime/secondarySalaryType/secondaryFactor werden vom Processor nicht ausgewertet (nur SalaryType.minimalTime greift). | Processor |
| Agreement-SalaryTypes ungenutzt/ungespeichert | wageSalary/overTimeSalary/defaultSalary am Vertrag werden vom Processor nicht konsumiert; der FE-Vertragsdialog speichert sie zudem nicht (nur name/description). | Entity + FE-Dialog |
| Feiertag global (nicht mandanten-/regionsspezifisch) | publicHolidayRepo->findOneBy(['date' => $calendarDay]) prüft nur das Datum – keine Region/Mandant. | generate() |
getMonthRange Jahres-Default | Ohne year wird das aktuelle Jahr genommen; Generierung für vergangene Jahre nur über die interne API mit year-Argument. | getMonthRange |
Keine type-Validierung | SalaryType.type bleibt ein freier String ohne Enum-Constraint; Gültigkeit nur über FE-Dropdown. | Entity |
| Hartkodierte FE-Labels | „Zuschlag in %", „Minimum", „Standard für Feiertage", „Lohnart" nicht übersetzt. | FE-Templates |
journalDate nur Backend | Das Frontend kennt kein journal_date; Debugging von Nachtschicht-Verteilungen nur über die DB möglich. | FE-Modelle |
| Namens-Tippfehler | Command …saleryelements…, Klasse GenerateMounthlySalaryElementsCommand, FE-Service SalaryElementsSerivice. | diverse |
| Keine Tests | Für die Generierungslogik existieren keine automatisierten Tests. | – |
12. Referenz: alle Fundstellen
Backend
| Datei | Rolle |
|---|---|
| Processor/Salary/SalaryElementGenerationProcessor.php | Journalbasierte Generierung (Kern) |
| ViewModel/PreCalculatedBooking.php | Rechenobjekt Anwesenheits-Intervall |
| Controller/SalaryElementController.php | CRUD + generate/worker/{id} |
| Command/GenerateMounthlySalaryElementsCommand.php | Zulagen-Monatsgenerierung (Altpfad) |
| Command/TestCommand.php | Test-Aufruf (defekt) |
| Entity/SalaryElement.php | profileDate, journalDate, salaryUnit |
| Entity/SalaryType.php | type, amount, minimalTime, isStandardForHoliday, absenceCodes |
| Entity/SalaryAgreement.php | Lohnvertrag |
| Entity/SalaryAgreementDay.php | Wochentagsregeln |
| Entity/SalaryAgreementLine.php | Zeitfenster → Lohnart |
| Entity/AbsenceCode.php | salaryType-Verknüpfung |
| Entity/Worker.php | salaryAgreement, salaryElements |
| Entity/WorkerSalaryType.php | Zulagen-Zuordnung (Altpfad) |
| Repository/SalaryElementRepository.php | getByWorkerAndRange, getByWorkerAndJournalDate |
| Repository/AbsenceRepository.php | getAbsencesForWorkerInRangeByType (type optional) |
| Repository/WorkerRepository.php | getWorkersByType, getActiveWorkers |
| migrations Version20260302140928 / …319151007 / …323104345 / …630141454 | Schema |
Frontend (relativ zu biz.Suite_Frontend/src/app/main/apps/)
| Datei/Verzeichnis | Rolle |
|---|---|
organization-management/salary-managment/salary-elements/salary-element-generation-dialog/ | Generierungs-Dialog (Trigger) |
organization-management/salary-managment/salary-elements/salary-elements.service.ts | generateSalaryElements → POST generate |
organization-management/salary-managment/salary-elements/ | Admin-Liste + Dialog + Access-Service |
organization-management/salary-managment/salary-types/ | Lohnart-Modell + Dialog (amount/minimal_time/is_standard_for_holiday) |
organization-management/salary-managment/salary-agreements/ + dialogs/ | Verträge, Mitglieder-Zuordnung |
organization-management/salary-managment/salary-agreement-day/ | Vertrags-Lines |
hrm/absenceCode/ | Abwesenheitscode ↔ Lohnart |
hrm/worker-allowances/ | Zulagen-Report (Altpfad) |
hrm/workers/ (worker.model.ts, details/worker-referenced-salary-types/) | salary_agreement, Zulagen-Zuordnung |
Ende der Dokumentation.